%% Author: vivimice
%% Created: Feb 3, 2012
%% Description: TODO: single line text input
-module(input_field).

%%
%% Include files
%%
-include("keys.hrl").
-include("tchar.hrl").
-include("tstring.hrl").

%%
%% Records
%%
-record(config, {context, type, max_width, validator}).
-record(state, {context, before_cursor, after_cursor, config, showing_warning}).

-define(BEFORE_CURSOR, State#state.before_cursor).
-define(AFTER_CURSOR, State#state.after_cursor).
-define(CONFIG, State#state.config).
-define(CONTEXT, ?CONFIG#config.context).

%%
%% Exported Functions
%%
-export([new_config/1, get_text/1]).
-export([set_type/2, set_max_width/2, set_validator/2]).

%%
%% Internal Exports
%%
-export([loop/1]).


%%
%% API Functions
%%

% create option for input_field, Type must be either text or password, MaxWidth must be a positive integer.
new_config(Context) ->
	set_validator(
	  set_max_width(
		set_type(#config{context=Context}, text),
		80), 
	  fun (_) -> ok end).

set_type(Config, password) ->
	Config#config{type = password};
set_type(Config, text) ->
	Config#config{type = text}.

set_max_width(Config, MaxWidth) when is_number(MaxWidth), MaxWidth > 0 ->
	Config#config{max_width = MaxWidth}.

set_validator(Config, Validator) when is_function(Validator, 1) ->
	Config#config{validator = Validator}.

% get text or password from client, use create_options to generation 2nd parameter
% returns tstring
get_text(Config) when is_record(Config, config) ->
	State = #state{config = Config,
				   before_cursor = tstring:new(),
				   after_cursor = tstring:new(),
				   showing_warning = false},
	?MODULE:loop(State);
get_text(_) ->
	erlang:error(badarg).

%%
%% Local Functions
%%

% Buffer stores binaries, ?BEFORE_CURSOR stores data before cursor, ?AFTER_CURSOR stores data after cursor
loop(State) ->
	case tio_manager:read(?CONTEXT) of
		
		{tchar, TChar} ->
			BeforeCursorWidth = ?TSTRING_WIDTH(?BEFORE_CURSOR),
			AfterCursorWidth = ?TSTRING_WIDTH(?AFTER_CURSOR),
			WidthOfTChar = ?TCHAR_WIDTH(TChar),
			if
				BeforeCursorWidth + WidthOfTChar + AfterCursorWidth =< ?CONFIG#config.max_width ->
					NewBeforeCursor = tstring:append(?BEFORE_CURSOR, TChar),
					write(?CONTEXT, TChar, ?CONFIG),
					write(?CONTEXT, ?AFTER_CURSOR, ?CONFIG),
					tio_manager:move_cursor_horizontally(?CONTEXT, -AfterCursorWidth),
					tio_manager:flush(?CONTEXT),
					?MODULE:loop(State#state{before_cursor = NewBeforeCursor});
				
				true ->
					% width excceeds max_width specified in options
					?MODULE:loop(State)
			end;
		
		{control, ?KEY_HOME} ->
			tio_manager:move_cursor_horizontally(?CONTEXT, -?TSTRING_WIDTH(?BEFORE_CURSOR)),
			tio_manager:flush(?CONTEXT),
			?MODULE:loop(State#state{before_cursor = tstring:new(),
									 after_cursor = tstring:append(?BEFORE_CURSOR, ?AFTER_CURSOR)});
		
		{control, ?KEY_END} ->
			tio_manager:move_cursor_horizontally(?CONTEXT, ?TSTRING_WIDTH(?AFTER_CURSOR)),
			tio_manager:flush(?CONTEXT),
			?MODULE:loop(State#state{before_cursor = tstring:append(?BEFORE_CURSOR, ?AFTER_CURSOR),
									 after_cursor = tstring:new()});
		
		{control, ?KEY_LEFT} when ?TSTRING_LENGTH(?BEFORE_CURSOR) > 0 ->
			TCharBeforeCursor = tstring:last(?BEFORE_CURSOR),
			tio_manager:move_cursor_horizontally(?CONTEXT, -?TCHAR_WIDTH(TCharBeforeCursor)),
			tio_manager:flush(?CONTEXT),
			?MODULE:loop(State#state{before_cursor = tstring:substring(?BEFORE_CURSOR, ?TSTRING_LENGTH(?BEFORE_CURSOR) - 1),
									 after_cursor = tstring:append(tstring:value_of(TCharBeforeCursor), ?AFTER_CURSOR)});
		
		{control, ?KEY_RIGHT} when ?TSTRING_LENGTH(?AFTER_CURSOR) > 0 ->
			TCharAfterCursor = tstring:head(?AFTER_CURSOR),
			tio_manager:move_cursor_horizontally(?CONTEXT, ?TCHAR_WIDTH(TCharAfterCursor)),
			tio_manager:flush(?CONTEXT),
			?MODULE:loop(State#state{before_cursor = tstring:append(?BEFORE_CURSOR, TCharAfterCursor),
									 after_cursor = tstring:substring(?AFTER_CURSOR, 1, ?TSTRING_LENGTH(?AFTER_CURSOR) - 1)});
	
		{control, ?KEY_DELETE} when ?TSTRING_LENGTH(?AFTER_CURSOR) > 0 ->
			RemovedTChar = tstring:head(?AFTER_CURSOR),
			NewAfterCursor = tstring:substring(?AFTER_CURSOR, 1, ?TSTRING_LENGTH(?AFTER_CURSOR) - 1),
			write(?CONTEXT, NewAfterCursor, ?CONFIG),
			tio_manager:write(?CONTEXT, tstring:repeat(?TC_SPACE, ?TCHAR_WIDTH(RemovedTChar))),
			tio_manager:move_cursor_horizontally(?CONTEXT, -?TSTRING_WIDTH(?AFTER_CURSOR)),
			tio_manager:flush(?CONTEXT),
			?MODULE:loop(State#state{after_cursor=NewAfterCursor});
		
		{control, ?KEY_BACKSPACE} when ?TSTRING_LENGTH(?BEFORE_CURSOR) > 0 ->
			RemovedTChar = tstring:last(?BEFORE_CURSOR),
			RemovedTCharWidth = ?TCHAR_WIDTH(RemovedTChar),
			NewBeforeCursor = tstring:substring(?BEFORE_CURSOR, ?TSTRING_LENGTH(?BEFORE_CURSOR) - 1),
			tio_manager:move_cursor_horizontally(?CONTEXT, -RemovedTCharWidth),
			write(?CONTEXT, ?AFTER_CURSOR, ?CONFIG),
			tio_manager:write(?CONTEXT, tstring:repeat(?TC_SPACE, RemovedTCharWidth)),
			tio_manager:move_cursor_horizontally(?CONTEXT, -(?TSTRING_WIDTH(?AFTER_CURSOR) + RemovedTCharWidth)),
			tio_manager:flush(?CONTEXT),
			?MODULE:loop(State#state{before_cursor=NewBeforeCursor});
		
		% input finished
		{control, ?KEY_ENTER} ->
			Result = tstring:append(?BEFORE_CURSOR, ?AFTER_CURSOR),
			Validator = ?CONFIG#config.validator,
			case erlang:apply(Validator, [Result]) of
				ok ->
					Result;
				
				{error, Reason} when ?IS_TSTRING(Reason) ->
					if
						?TSTRING_LENGTH(Reason) > ?CONFIG#config.max_width ->
							Msg = tstring:crop_to_width(Reason, ?CONFIG#config.max_width);
						true ->
							Msg = tstring:pad_to_width(Reason, ?CONFIG#config.max_width, ?TC_SPACE, right)
					end,
					MsgLen = ?TSTRING_WIDTH(Msg),
					% write error reason
 					tio_manager:bell(?CONTEXT),
					tio_manager:move_cursor_horizontally(?CONTEXT, -?TSTRING_WIDTH(?BEFORE_CURSOR)),
					tio_manager:write(?CONTEXT, <<"\x1b[1;5;33;41m">>),
					tio_manager:write(?CONTEXT, Msg),
					tio_manager:write(?CONTEXT, <<"\x1b[0m">>),
					tio_manager:flush(?CONTEXT),
					% get any key to continue
					tio_manager:read(?CONTEXT),
					% clear error
					tio_manager:move_cursor_horizontally(?CONTEXT, -MsgLen),
					tio_manager:write(?CONTEXT, tstring:repeat(?TC_SPACE, MsgLen)),
					% restore previous display
					tio_manager:move_cursor_horizontally(?CONTEXT, -MsgLen),
					write(?CONTEXT, ?BEFORE_CURSOR, ?CONFIG),
					write(?CONTEXT, ?AFTER_CURSOR, ?CONFIG),
					tio_manager:move_cursor_horizontally(?CONTEXT, -?TSTRING_WIDTH(?AFTER_CURSOR)),
					tio_manager:flush(?CONTEXT),
					?MODULE:loop(State);
				
				Other ->
					logger:warn("ok or {error, Message::TString} expected for validate result: ~w. but ~w encountered", [Result, Other]),
					?MODULE:loop(State)
			end;
		
		_Other ->
			?MODULE:loop(State)
	end.

write(Context, TChar, Config) 
  when Config#config.type == password,
	   ?IS_TCHAR(TChar)->
	tio_manager:write(Context, tstring:repeat(tchar:new_ansi($*), ?TCHAR_WIDTH(TChar)));
write(Context, TString, Config) 
  when Config#config.type == password,
	   ?IS_TSTRING(TString)->
	tio_manager:write(Context, tstring:repeat(tchar:new_ansi($*), ?TSTRING_WIDTH(TString)));
write(Context, Data, _) ->
	tio_manager:write(Context, Data).
